#include <GL/glew.h>
#include <GL/wglew.h>
#include "context_wgl.h"
#include "framework/logger.h"

#define	CYB_WNDCLASSNAME "CYBWND10"

namespace cyb
{

static std::string GetDebugSourceString( GLenum source )
{
	switch (source )
	{
	case GL_DEBUG_SOURCE_API_ARB:				return "OpenGL";
	case GL_DEBUG_SOURCE_WINDOW_SYSTEM_ARB:		return "Windows";
	case GL_DEBUG_SOURCE_SHADER_COMPILER_ARB:	return "Shader Compiler";
	case GL_DEBUG_SOURCE_THIRD_PARTY_ARB:		return "Third Party";
	case GL_DEBUG_SOURCE_APPLICATION_ARB:		return "Application";
	case GL_DEBUG_SOURCE_OTHER_ARB:				return "Other";
	}

	return "Unknown";
}

static std::string GetDebugTypeString( GLenum type )
{
	switch ( type )
	{
	case GL_DEBUG_TYPE_ERROR_ARB:				return "Error";
	case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR_ARB:	return "Deprecated behavior";
	case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR_ARB:	return "Undefined behavior";
	case GL_DEBUG_TYPE_PORTABILITY_ARB:			return "Portability";
	case GL_DEBUG_TYPE_PERFORMANCE_ARB:			return "Performance";
	case GL_DEBUG_TYPE_OTHER_ARB:				return "Other";
	}

	return "Unknown";
}

static std::string GetDebugSeveretyString( GLenum severity )
{
	switch ( severity )
	{
	case GL_DEBUG_SEVERITY_HIGH_ARB:			return "High";
	case GL_DEBUG_SEVERITY_MEDIUM_ARB:			return "Medium";
	case GL_DEBUG_SEVERITY_LOW_ARB:				return "Low";
	}

	return "Unknown";
}

static GLvoid DebugCallback( GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, GLvoid* userParam )
{ 
	CYB_CHECK( false, "OpenGL debug message:",
		"\n\tSource: ", GetDebugSourceString( source ),
		"\n\tType: ", GetDebugTypeString( type ), 
		"\n\tID: ", id, 
		"\n\tSeverity: ", GetDebugSeveretyString( severity ), 
		"\n\tMessage: ", message );
}


static HGLRC Win32_CreateDummyContext( HDC hdc )
{
	PIXELFORMATDESCRIPTOR pfd;
	memset( &pfd, 0, sizeof( pfd) );
	pfd.nSize = sizeof( PIXELFORMATDESCRIPTOR );
	pfd.nVersion = 1;
	pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
	pfd.iPixelType = PFD_TYPE_RGBA;
	pfd.cColorBits = 32;
	pfd.cAlphaBits = 8;
	pfd.cDepthBits = 24;
	pfd.cStencilBits = 8;
	pfd.iLayerType = PFD_MAIN_PLANE;

	int iPixelFormat = ChoosePixelFormat( hdc, &pfd );
	CYB_FATAL( iPixelFormat != 0, "ChoosePixelFormat failed!" );

	DescribePixelFormat( hdc, iPixelFormat, sizeof( PIXELFORMATDESCRIPTOR ), &pfd );

	CYB_DEBUG( "Pixel format:\n",
		"\tiPixelType  : ", (int) pfd.iPixelType,	"\n",
		"\tcColorBits  : ", (int) pfd.cColorBits,	"\n",
		"\tcAlphaBits  : ", (int) pfd.cAlphaBits,	"\n",
		"\tcDepthBits  : ", (int) pfd.cDepthBits,	"\n",
		"\tcStencilBits: ", (int) pfd.cStencilBits );

	BOOL bResult = SetPixelFormat( hdc, iPixelFormat, &pfd );
	CYB_FATAL( bResult != FALSE, "SetPixelFormat failed (code: ", GetLastError(), ")" );

	HGLRC context = wglCreateContext( hdc );
	CYB_FATAL( context != NULL, "wglCreateContext failed!" );

	bResult = wglMakeCurrent( hdc, context );
	CYB_FATAL( bResult != FALSE, "wglMakeCurrent failed!" );

	return context;
}

static HGLRC Win32_CreateContext( HDC hdc )
{
	HGLRC tempContext = Win32_CreateDummyContext( hdc );

	GLenum err = glewInit();
	CYB_FATAL( err == GLEW_OK, "glewInit failed: ", glewGetErrorString( err ) );
	CYB_DEBUG( "Using GLEW ", glewGetString( GLEW_VERSION ) );

	int32_t attrs[] = {
		WGL_SAMPLE_BUFFERS_ARB, 0,
		WGL_SAMPLES_ARB, 0,
		WGL_SUPPORT_OPENGL_ARB, true,
		WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB,
		WGL_DRAW_TO_WINDOW_ARB, true,
		WGL_DOUBLE_BUFFER_ARB, true,
		WGL_COLOR_BITS_ARB, 32,
		WGL_ALPHA_BITS_ARB, 8,
		WGL_DEPTH_BITS_ARB, 24,
		WGL_STENCIL_BITS_ARB, 8,
		0 
	};

	uint32_t numFormats = 0;
	int iPixelFormat = 0;
	PIXELFORMATDESCRIPTOR pfd;

	do 
	{
		int iResult = wglChoosePixelFormatARB( hdc, attrs, NULL, 1, &iPixelFormat, &numFormats );
		if ( iResult == 0 || numFormats == 0 )
		{
			attrs[3] >>= 1;
			attrs[1] = attrs[3] == 0 ? 0 : 1;
		}
	} while ( numFormats == 0 );

	DescribePixelFormat( hdc, iPixelFormat, sizeof( PIXELFORMATDESCRIPTOR ), &pfd );
	CYB_DEBUG( "Pixel format:\n",
		"\tiPixelType  : ", (int) pfd.iPixelType,	"\n",
		"\tcColorBits  : ", (int) pfd.cColorBits,	"\n",
		"\tcAlphaBits  : ", (int) pfd.cAlphaBits,	"\n",
		"\tcDepthBits  : ", (int) pfd.cDepthBits,	"\n",
		"\tcStencilBits: ", (int) pfd.cStencilBits );
	BOOL bResult = SetPixelFormat( hdc, iPixelFormat, &pfd );
	CYB_FATAL( bResult != FALSE, "SetPixelFormat failed (code: ", GetLastError(), ")" );
	
	int32_t contextAttrs[9] = {
		WGL_CONTEXT_MAJOR_VERSION_ARB, 4,
		WGL_CONTEXT_MINOR_VERSION_ARB, 4,
		WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_DEBUG_BIT_ARB,
		WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
		0
	};

	HGLRC context = wglCreateContextAttribsARB( hdc, 0, contextAttrs );
	CYB_FATAL( context != NULL, "wglCreateContextAttribsARB failed (code: ", GetLastError(), ")" );

	wglMakeCurrent( NULL, NULL );
	wglDeleteContext( tempContext );

	bResult = wglMakeCurrent( hdc, context );
	CYB_FATAL( bResult != FALSE, "wglMakeCurrent failed!" );

	return context;
}

static LRESULT CALLBACK StaticWndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
	Context_Win* window = NULL;

	if ( uMsg == WM_CREATE )
	{
		window = (Context_Win*) ((LPCREATESTRUCT) lParam)->lpCreateParams;
		SetWindowLongPtr( hWnd, GWL_USERDATA, (LONG_PTR) window );
	}
	else
	{
		window = (Context_Win*) GetWindowLongPtr( hWnd, GWL_USERDATA );
		if ( !window )
		{
			return DefWindowProc( hWnd, uMsg, wParam, lParam );
		}
	}

	return window->WndProc( hWnd, uMsg, wParam, lParam );
}

Context_Win::Context_Win( const Dimension2i& resolution ) :
	m_resolution( resolution )
{
	OpenWindow();
}

Context_Win::~Context_Win()
{
	DestroyWindow( m_hwnd );
}

void Context_Win::OpenWindow()
{
	WNDCLASS wc;
	wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	wc.lpfnWndProc = StaticWndProc;
	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hInstance = GetModuleHandle( NULL );
	wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
	wc.hCursor = LoadCursor( NULL, IDC_ARROW );
	wc.hbrBackground = NULL;
	wc.lpszMenuName = NULL;
	wc.lpszClassName = CYB_WNDCLASSNAME;
	wc.hIcon = LoadIcon( NULL, IDI_WINLOGO );

	ATOM result = RegisterClass( &wc );
	CYB_FATAL( result != 0, "RegisterClass failed (code: ", GetLastError(), ")" );

	DWORD dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
	DWORD dwStyle = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;

	RECT rect;
	rect.left = 0;
	rect.right = m_resolution.width;
	rect.top = 0;
	rect.bottom = m_resolution.height;
	AdjustWindowRectEx( &rect, dwStyle, false, dwExStyle );

	m_hwnd = CreateWindowEx(
		dwExStyle,
		CYB_WNDCLASSNAME,
		"Cyb Engine",		// default window title
		dwStyle,
		CW_USEDEFAULT, CW_USEDEFAULT,
		rect.right - rect.left,
		rect.bottom - rect.top,
		NULL,
		NULL,
		GetModuleHandle( NULL ),
		this );

	CYB_FATAL( m_hwnd != NULL, "Failed to create window (code: ", GetLastError(), ")" );

	ShowWindow( m_hwnd, SW_SHOW );
	UpdateWindow( m_hwnd );
}

LRESULT Context_Win::WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
	for ( auto& input : m_inputListeners )
	{
		OnMsgProc( uMsg, wParam, lParam, input );
	}

	switch ( uMsg )
	{
	case WM_CREATE:
		m_hdc = GetDC( hWnd );
		m_hglrc = Win32_CreateContext( m_hdc );
		glDebugMessageCallbackARB( (GLDEBUGPROCARB) &DebugCallback, NULL );
		glEnable( GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB );
		m_isOpen = true;
		break;
	case WM_SIZE:
		Resize( Dimension2i( LOWORD( lParam ), HIWORD( lParam ) ) );
		break;
	case WM_DESTROY:
	case WM_CLOSE:
		m_isOpen = false;
		PostQuitMessage( 0 );
		break;
	default:
		break;
	}

	return DefWindowProc( hWnd, uMsg, wParam, lParam );
}

void Context_Win::OnMsgProc( UINT uMsg, WPARAM wParam, LPARAM lParam, std::shared_ptr<InputListener> input )
{
	bool result = false;

	switch ( uMsg )
	{
	case WM_MOUSEMOVE:
		if ( input->mouseListener )
		{
			result = input->mouseListener->OnMouseMove( LOWORD( lParam ), HIWORD( lParam ) );
		}
		break;
	case WM_LBUTTONDOWN:
		if ( input->mouseListener )
		{
			result = input->mouseListener->OnMouseButtonDown( LOWORD( lParam ), HIWORD( lParam ), MouseButton::Left );
		}
		break;
	case WM_LBUTTONUP:
		if ( input->mouseListener )
		{
			result = input->mouseListener->OnMouseButtonUp( LOWORD( lParam ), HIWORD( lParam ), MouseButton::Left );
		}
		break;
	case WM_MBUTTONDOWN:
		if ( input->mouseListener )
		{
			result = input->mouseListener->OnMouseButtonDown( LOWORD( lParam ), HIWORD( lParam ), MouseButton::Middle );
		}
		break;
	case WM_MBUTTONUP:
		if ( input->mouseListener )
		{
			result = input->mouseListener->OnMouseButtonUp( LOWORD( lParam ), HIWORD( lParam ), MouseButton::Middle );
		}
		break;
	case WM_RBUTTONDOWN:
		if ( input->mouseListener )
		{
			result = input->mouseListener->OnMouseButtonDown( LOWORD( lParam ), HIWORD( lParam ), MouseButton::Right );
		}
		break;
	case WM_RBUTTONUP:
		if ( input->mouseListener )
		{
			result = input->mouseListener->OnMouseButtonUp( LOWORD( lParam ), HIWORD( lParam ), MouseButton::Right );
		}
		break;
	}
}

void Context_Win::ProcessEvents()
{
	MSG msg;

	while ( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
	{
		TranslateMessage( &msg );
		DispatchMessage( &msg );
	}
}

void Context_Win::Resize( const Dimension2i& resolution )
{
	m_resolution = resolution;
}

void Context_Win::Title( const std::string& windowTitle )
{
	SetWindowText( m_hwnd, windowTitle.c_str() );
}

void Context_Win::ShowMouseCursor( bool show )
{
	ShowCursor( show );
}

void Context_Win::SwapBuffers()
{
	::SwapBuffers( m_hdc );
}

}	// namespace cyb